Skip to content

fix(debug): normalize Windows PATH env keys - #1029

Closed
xclear-cast wants to merge 1 commit into
microsoft:mainfrom
xclear-cast:codex/normalize-windows-env-path
Closed

xclear-cast wants to merge 1 commit into
microsoft:mainfrom
xclear-cast:codex/normalize-windows-env-path

Conversation

@xclear-cast

Copy link
Copy Markdown

Summary

  • normalize Windows search-path environment keys to the debugger's preferred Path casing before handing env vars to debugpy
  • merge alternate PATH values into Path by path segment so duplicate keys and duplicate path entries are avoided
  • add a VS Code-hosted unit test covering a launch env that supplies PATH while Windows uses Path

Fixes #261.

Validation

pm ci --ignore-scripts to install JS dependencies after plain
pm ci hit a local native build-tools blocker in @vscode/windows-process-tree (MSB8040, missing Spectre-mitigated VC libraries)

pm run compile-tests

pm run lint

pm run format-check

pm run compile

  • TEST_GREP='duplicate Windows search path' node ./out/test/runTest.js
  • git diff --check

The focused VS Code-hosted unit test passed: 1 passing.

name: 'Python Debug Test',
type: 'debugpy',
request: 'launch',
env: launchEnv,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot generated:
-95

Test fails on non-Windows CI (Skeptic, structurally confirmed). sinon.stub(platform, 'getOSType').returns(platform.OSType.Windows) makes getSearchPathEnvVarNames() return ['Path', 'PATH'], but path.delimiter remains : on Linux/macOS — it's a Node.js runtime constant, not controlled by the stub. The production code in normalizeSearchPathEnvVar uses path.delimiter to split/join segments, so on Linux env.Path will contain : delimiters. The assertions then split by hardcoded ';', which won't match.

Fix options:

  1. Use path.delimiter in assertions instead of hardcoded ';'
  2. Also stub/mock path.delimiter to ';'
  3. Skip the test on non-Windows: if (process.platform !== 'win32') return

[verified]

name: 'Python Debug Test',
type: 'debugpy',
request: 'launch',
env: launchEnv,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot generated:
-91

Partial assertions violate test precision (Rules). .to.contain(...) would still pass if env.Path contained extra duplicates, wrong delimiters, or unrelated segments — exactly the regression class this test should catch:

expect(env.Path).to.contain('C:\\Project\\.venv\\Scripts');
expect(env.Path).to.contain('C:\\Tools');

Fix: Capture the exact runtime value of env.Path (via debugger or a passing run) and assert with .to.equal('<exact-full-string>'). This ensures the full merged, deduplicated, correctly-delimited value is validated.

[verified]


expect(program).to.be.equal(undefined, 'Not undefined');
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot generated:
-102

Missing edge case coverage (Skeptic + Architect). The test only covers the internalConsole code path (which triggers mergeVariables(process.env, env)). Consider adding:

  1. A non-internalConsole mode test to verify the earlier normalization calls work independently.
  2. A test confirming Linux/macOS (PATH-only, single key) is unaffected by normalization.
  3. [unverified] On Windows Node.js, process.env is case-insensitive, so process.env.Path = 'X'; process.env.PATH = 'Y' may alias to the same entry. The test's process.env setup may not create the two-key scenario it appears to — the args.env plain-object path still exercises the core logic, but the process.env interaction is less realistic than intended.

[verified]


'use strict';

import * as path from 'path';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot generated:
,12

Readonly parameters (Rules). Both new helpers accept names: ('Path' | 'PATH')[] but never mutate it. vars in getSearchPathEnvVar is only read (.map, .find). Per the repo's immutability-by-default convention:

function getSearchPathEnvVar(vars: Readonly<EnvironmentVariables>, names: readonly ('Path' | 'PATH')[]): ...
function normalizeSearchPathEnvVar(env: EnvironmentVariables, names: readonly ('Path' | 'PATH')[]) { ...

(env is intentionally mutable since the function modifies it.)

[verified]

const currentSegments = new Set(currentValue.split(path.delimiter).filter((item) => item.length > 0));
const alternateSegments = alternateValue
.split(path.delimiter)
.filter((item) => item.length > 0 && !currentSegments.has(item));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot generated:
-35

Case-sensitive path segment deduplication on a case-insensitive filesystem (Skeptic, structurally confirmed; Architect concurs). The Set.has() check uses === semantics. On Windows, C:\Windows\System32 and c:\windows\system32 refer to the same directory but would survive deduplication.

The Advocate correctly notes this matches pre-existing behavior in appendPath/appendPaths. However, the PR description claims "duplicate path entries are avoided" — case-insensitive duplicates are not. Consider normalizing with .toLowerCase() for the lookup Set while preserving original casing in the output, or document this as a known limitation.

[verified]


// "overwrite: true" to ensure that debug-configuration env variable values
// take precedence over env file.
envParser.mergeVariables(debugLaunchEnvVars, env, { overwrite: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot generated:
-100

Root cause assessment — symptom treatment, not root fix (Architect + Skeptic). Five normalizeSearchPathEnvVar calls are needed because mergeVariables in environment.ts only excludes getSearchPathEnvVarNames()[0] (i.e., Path), letting PATH through on every merge. The normalization is idempotent and correct (Advocate), but fragile — any future merge/append without a following normalize silently regresses the bug (Architect).

Recommended follow-up: fix environment.ts line ~55 to settingsNotToMerge = ['PYTHONPATH', ...getSearchPathEnvVarNames()]. This one-line change would eliminate most normalize calls and protect all consumers of mergeVariables, not just the debugger. If not addressed in this PR, add a TODO comment explaining why each normalize call exists.

[verified]

@xclear-cast xclear-cast closed this by deleting the head repository Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to debug tests: Invalid message: Found duplicate in "env": PATH

2 participants